﻿// (c) Comkarl Calipso
// By Matías Fidemraizer (http://www.matiasfidemraizer.com)
// -------------------------------------------------
// http://calipso.codeplex.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Comkarl.Web.Compression;
using Comkarl.Web.Configuration;
using Comkarl.Web.Reflection;
using Comkarl.Web.Resources;
using Comkarl.Web.Serialization;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.UI;
using System.Xml;

namespace Comkarl.Web.UI
{
    /// <summary>
    /// Represents a JavaScript manager, which handles includes, ScriptControl registration, and more.
    /// </summary>
    /// <remarks>
    /// <para>JavaScriptManager can be both a server control and a standalone class. But non-infrastructure code - this is ASP.NET Calipso-based projects - get
    /// instances of this class by accessing <see cref="Comkarl.Web.UI.Controls.ScriptControl.JavaScriptManager"/> property.</para>
    /// 
    /// <h3>Quick links</h3>
    /// <list type="bullet">
    ///     <item>
    ///         <a href="#concepts">1.0 Concepts</a>
    ///         <list type="bullet">
    ///             <item>
    ///                 <a href="#configuringjavascriptmanager">1.1 Configuring JavaScriptManager</a>
    ///             
    ///                 <list type="bullet">
    ///                     <item><a href="#runtimeincludes">1.1.0 Adding includes during run-time in ScriptControl-derived classes</a></item>
    ///                 </list>
    ///             </item>
    ///         </list>
    ///     </item>
    ///     <item>
    ///         <a href="#workingwithjavascriptmanager">2.0 Working with JavaScriptManager</a>
    ///         <list type="bullet">
    ///             <item>
    ///                 <a href="#javascriptreferences">2.1 JavaScript references</a>
    ///                 
    ///                 <list type="bullet">
    ///                     <item><a href="#perpagejavascriptmanager">2.1.0     Per-page JavaScriptManager: the way to go</a></item>
    ///                     <item><a href="#standalonereferences">2.1.1     Standalone references</a></item>
    ///                     <item><a href="#javascriptmanagermodes">2.1.2   JavaScriptManager debug and release mode: combination and minification</a></item>
    ///                     <item><a href="#referenceincludeorder">2.1.3    Order of script inclusion in rendered pages</a></item>
    ///                     <item><a href="#dependencyreferences">2.1.4   Dependency references: changing <c>SCRIPT</c> tag rendering priority</a></item>
    ///                 </list>
    ///             </item>
    ///             <item><a href="#builtindependencies">2.2  Built-in JavaScript dependencies</a></item>
    ///             <item><a href="#overriddingincludes">2.3    Overridding includes</a></item>
    ///         </list>
    ///     </item>
    /// </list>
    /// 
    /// 
    /// <a name="concepts" />
    /// <h3>1.0 Concepts</h3>
    /// <para>
    /// There are some concepts that should be taken in account about JavaScriptManager. In fact, these defines a lot of things that will be important in order to
    /// understand how it works. 
    /// 
    /// <list type="table">
    ///     <listheader>
    ///         <term>Concept</term>
    ///         <description>Explanation</description>
    ///     </listheader>
    ///     <item>
    ///         <term>Script control</term>
    ///         <description>
    ///             An script control is a regular server control (i.e. WebControl-derived class) that implements a set of new features that allow them to have a client-side Web browser counterpart.
    ///         </description>
    ///     </item>
    ///     <item>
    ///         <term>Include</term>
    ///         <description>
    ///             An include is a pointer to some file. It may point to a physical file stored in the server file system, a file embedded in some .NET assembly or in a remote site by URL.
    ///             Although these are configured, this does not mean that are going to be added to Web pages managed by ASP.NET application. Includes are just there to be referenced.
    ///         </description>
    ///     </item>
    ///     <item>
    ///         <term>Reference</term>
    ///         <description>
    ///            A reference is a pointer to an existing include. Referencing an include means adding a <c><script></script></c> HTML element to the rendered page.
    ///         </description>
    ///     </item>
    ///     <item>
    ///         <term>Resource</term>
    ///         <description>
    ///             A resource is a localization RESX (resource file) of strings compiled into a JavaScript file. ASP.NET Calipso makes this compilation during each Visual Studio project or solution build.
    ///             Resources are referenced by script controls.
    ///         </description>
    ///     </item>
    ///     <item>
    ///         <term>Cache</term>
    ///         <description>
    ///             JavaScriptManager manages JavaScript file dependencies. In order to optimize access to some of them, it builds a file cache in the server's file system.
    ///         </description>
    ///     </item>
    /// </list>
    /// </para>
    /// <br />
    /// <br />
    /// <a name="configuringjavascriptmanager" />
    /// <h4>1.1 Configuring JavaScriptManager</h4>
    /// 
    /// There are three configuration points for JavaScriptManager:
    /// <list type="bullet">
    ///     <item><description>Calispo configuration section on Web.config (usually it is an standalone file called "Calipso.config" in the ASP.NET application root)</description></item>
    ///     <item><description>Pages and master pages</description></item>
    ///     <item><description>Script control server code</description></item>
    /// </list>
    /// 
    /// <a name="configsection" />
    /// <h5>1.1.0 Calipso configuration section on Web.config</h5>
    /// 
    /// <para>
    /// First of all, locate <strong>Calipso.config</strong> configuration file in the root of ASP.NET application (if this file is not there, means that ASP.NET Calipso has been
    /// installed manually without NuGet package). 
    /// </para>
    /// 
    /// <para>
    /// <strong>Calipso.config</strong> has an XML configuration element called <strong>javascriptManager</strong> (i.e. <c><javascriptManager></c>) which is the one that can configure settings
    /// for later JavaScriptManager execution and build-time actions.
    /// </para>
    /// 
    /// <para>
    /// <c><javascriptManager></c> element has these mandatory attributes:
    /// <list type="table">
    ///     <listheader>
    ///         <term>Attribute</term>
    ///         <description>Description</description>
    ///     </listheader>
    ///     <item>
    ///          <term>cachePath</term>
    ///          <description>Defines a relative virtual path where cached, JavaScriptManager-generated script files will be stored in. Default is "~/cache".</description>
    ///     </item>
    ///     <item>
    ///         <term>cacheVersion</term>
    ///         <description>Sets a version number that will be appended to all cached script files in order to allow client-side script cache to be broken. Default is "1.0.0.0".</description>
    ///     </item>
    ///     <item>
    ///         <term>dependencyConventionPrefix</term>
    ///         <description>Defines the arbitrary dependency include identifier prefix. Default is "Dependency//".</description>
    ///     </item>
    ///     <item>
    ///         <term>resourceConventionPrefix</term>
    ///         <description>Defines the arbitrary resource include identifier prefix. Default is "Resources//".</description>
    ///     </item>
    ///     <item>
    ///         <term>afterLoadConventionPrefix</term>
    ///         <description>Defines the arbitrary after-load include identifier prefix. That is includes added using <see cref="JavaScriptManager.RegisterAfterLoad"/> method. Default is "AfterLoad//".</description>
    ///     </item>
    /// </list>
    /// </para>
    /// 
    /// <para>
    /// <c><javascriptManager></c> XML element supports a child one: <c><includes></includes></c>. Includes, as said before in "Concepts", are pointers to files, either physical, embedded into .NET assemblies or remote sites (URLs). 
    /// 
    /// In order to add an include, an <c><add /></c> child XML element must be added to parent <c><includes></c>. 
    /// 
    /// <example>
    ///     <h6>A sample include of file kind (physical file).</h6>
    ///     <para>Note that file kind requires a relative path, starting from the ASP.NET application root:</para>
    ///     <code>
    ///         <add id="SomePrefix//MyPhysicalFile" source="/Scripts/MyPhysicalFile.js" kind="File" />
    ///     </code>
    /// </example>
    /// 
    /// <example>
    ///     <h6>A sample include of resource kind (an embedded resource into some .NET assembly)</h6>
    ///     <para>Embedded resources source is a full assembly qualified name of the embedded file. That is, the full path to the embedded file comma assembly name:</para>
    ///     <code>
    ///         <add id="SomePrefix//MyEmbeddedFile" source="Samples.Scripts.MyEmbeddedFile.js, Samples" kind="Resource" />
    ///     </code>
    /// </example>
    /// 
    /// <example>
    ///     <h6>A sample include of ExternalUrl kind (a remotely-hosted file referenced by URL)</h6>
    ///     <para>ExternalUrl-kind incldues are provided by specifying an URL to a remote site:</para>
    ///     <code>
    ///         <add id="SomePrefix//MyRemoteFile" source="http://somedomain.com/myremotefile.js" kind="ExternalUrl" />
    ///     </code>
    /// </example>
    /// </para>
    /// 
    /// <a name="runtimeincludes" />
    /// <h5>1.1.1 Adding includes during run-time in ScriptControl-derived classes</h5>
    /// <para>
    /// Includes can be added during run-time by calling <see cref="RegisterInclude"/> method during script controls' life-cycle. Recommended: register includes during
    /// <strong>Init</strong> or <strong>Load</strong> ASP.NET control event invokation.
    /// 
    /// <para>ScriptControl-derived classes can access its assigned JavaScriptManager instance by accessing <strong>JavaScriptManager</strong> property.</para>
    /// 
    /// <example>
    ///     <h6>A sample include of file kind</h6>
    ///     <para>Note that file kind requires a relative path, starting from the ASP.NET application root:</para>
    ///     <code>
    ///         JavaScriptManager.RegisterInclude("SomePrefix//MyPhysicalFile", "/Scripts/MyPhysicalFile.js", Comkarl.Web.Resources.ResourceKind.File);
    ///     </code>
    /// </example>
    /// 
    /// <example>
    ///     <h6>A sample include of resource kind (an embedded resource into some .NET assembly)</h6>
    ///     <para>Embedded resources source is a full assembly qualified name of the embedded file. That is, the full path to the embedded file comma assembly name:</para>
    ///     <code>
    ///         JavaScriptManager.RegisterInclude("SomePrefix//MyEmbeddedFile", "Samples.Scripts.MyEmbeddedFile.js, Samples", Comkarl.Web.Resources.ResourceKind.Resource);
    ///     </code>
    /// </example>
    /// 
    /// <example>
    ///     <h6>A sample include of ExternalUrl kind (a remotely-hosted file referenced by URL)</h6>
    ///     <para>ExternalUrl-kind incldues are provided by specifying an URL to a remote site:</para>
    ///     <code>
    ///         JavaScriptManager.RegisterInclude("SomePrefix//MyRemoteFile", "http://somedomain.com/myremotefile.js", Comkarl.Web.Resources.ResourceKind.ExternalUrl);
    ///     </code>
    /// </example>
    /// </para>
    /// 
    /// <br />
    /// <br />
    /// <a name="workingwithjavascriptmanager" />
    /// <h4>2.0 Working with JavaScriptManager</h4>
    /// <para>
    /// JavaScriptManager manages dependencies by adding includes. But includes are just pointers. One of main advantages of using ASP.NET Calipso is that it handles the task of adding
    /// the necessary <c><script /></c> HTML elements. This is achieved thanks to the <strong>reference system</strong>.
    /// </para>
    /// <para>
    /// Once an include is loaded, any object having access to some JavaScriptManager can reference the include itself. <strong>The act of referencing an include is telling the JavaScriptManager
    /// to add a <c><script /></c> tag into the rendered HTML page</strong>. In fact, then every control, master page or page that references an include, it does by an identifier rather than giving the
    /// absolute file name, and dependencies location can be changed overtime without affecting the code base but by just modifying ASP.NET Calipso XML configuration or changing the code that registers
    /// an include during run-time.
    /// </para>
    /// <para>
    /// In addition to above described advantage, ASP.NET Calipso handles dependencies in terms of combining and minifying them in order to reduce Web sites' load time and bandwith usage.
    /// </para>
    /// <para>
    /// Once JavaScriptManager is configured, since it is a regular control, the next step is adding it to a Master Page or a regular page. JavaScriptManager is located in the 
    /// <strong>Comkarl.Web.UI namespace</strong>. As NuGet package register all ASP.NET Calipso control namespaces in the Web.config, it can be used anywhere in any Master Page or regular page
    /// by just coding a server <c><calipso:JavaScriptManager runat="server" /></c> tag. 
    /// 
    /// JavaScriptManager server control requires some mandatory attributes:
    /// 
    /// <list type="table">
    ///     <listheader><term>Attribute</term><description>Description</description></listheader>
    ///     <item>
    ///         <term>ID</term>
    ///         <description>An unique identifier for the control like any other ASP.NET server control.</description>
    ///     </item>
    ///     <item>
    ///         <term>CombinedFileName</term>
    ///         <description>When JavaScriptManager works with <c><compilation debug="false" /></c> (release mode), it combines its JavaScript references. This attribute
    ///         defines the name of the file that combines such JavaScript references.</description>
    ///     </item>
    /// </list>
    /// 
    /// Other detail is JavaScriptManager control can be appended to any part of a page: header, body... <c><script /></c> tags will be rendered in the place where JavaScriptManager server control was
    /// declared.
    /// </para>
    /// 
    /// <a name="javascriptreferences" />
    /// <h5>2.1 JavaScript references: design-time and run-time approach</h5>
    /// <para>
    /// There are two ways of adding JavaScript references: either in design-time or run-time. Actually, both are handled in the same way, but each have different use cases.
    /// </para>
    /// <h6>How to add design-time references</h6>
    /// <para>
    /// Design-time references are that ones added by a Master Page or regular page in their server markup. In order to add a reference, JavaScriptManager control has a child element called
    /// <strong>References</strong> which allow children elements <c><calipso:ResourceReference RefId="[Identifier of some loaded include]" /></c>.
    /// </para>
    /// <example>
    /// Next code listing would be a sample of how to add a reference:
    /// 
    /// <code>
    /// <calipso:JavaScriptManager ID="jsManager1" CombinedFileName="script1.js" runat="server">
    ///     <References>
    ///         <calipso:ResourceReference RefId="SomePrefix//MyFile" />
    ///     </References>
    /// </calipso:JavaScriptManager>
    /// </code>
    /// 
    /// <para>Also note that references are kind-agnostic. That is, a reference can point to any kind of include (File, Resource or ExternalUrl) and JavaScriptManager handles the operations needed to
    /// correctly add the script tag to the rendered page.</para>
    /// </example>
    /// <para>Usually, design-time references are that ones that are widely-required by any script control or anything in the Web site.</para>
    /// 
    /// <h6>How to add run-time references</h6>
    /// <para>Run-time references are added by calling <see cref="RegisterReference"/> method. The method takes as unique argument the identifier of the include to reference. See <see cref="RegisterReference"/> definition
    /// for further information on this topic.</para>
    /// 
    /// <a name="perpagejavascriptmanager" />
    /// <h5>2.1.0 Per-page JavaScriptManager: the way to go</h5>
    /// <para>
    /// In any Web site there are JavaScript files that are shared across all pages. For example, dependencies like <strong>jQuery framework</strong>. In addition, there are others that are specific to some page: a grid widget that
    /// lists users in a user management page. It is understandable that all pages should not be adding a <c><script /></c> tag to include the grid widget. Why other pages should increase their load time to parse an useless library?
    /// </para>
    /// <para>JavaScriptManager control can be added as child of any control, but most of the times these are added in Master Pages and regular pages (that is, content pages).</para>
    /// <para>Most projects will always add a JavaScriptManager control to their master pages and those will add shared JavaScript references like jQuery and others, and other JavaScriptManager control to content pages or nested master pages.</para>
    /// <para>
    /// Doing so allows JavaScriptManager to add dependencies specifically in the context that these are really required. In other words: the so-called grid widget would be referenced in a JavaScriptManager hosted by the content page "UserManager.aspx" 
    /// rather than its Master Page.
    /// </para>
    /// <para>
    /// Everything explained above takes an special importance when ASP.NET Calipso is working in release mode (i.e. <c><comilation debug="false"/></c> in regular Web.config), because JavaScript files will be combined into a combined and minifyed file
    /// for each JavaScriptManager control instance. For example, master page's JavaScript files would be combined into "master.js" file and an user management page JavaScript files would be combined into "usermanager.js". <strong>This adds efficiency
    /// in terms of Web browser/client-side page load-time and Web caching.</strong>
    /// </para>
    /// 
    /// <a name="standalonereferences" />
    /// <h5>2.1.1 Standalone references</h5>
    /// <para>While combination and minification of JavaScript files may work for a lot of cases, there are some that may require combination exclusions.</para>
    /// <para>This are standalone references. It is a way of telling JavaScriptManager that some reference should not end combined when context is in release mode.</para>
    /// 
    /// <h6>Design-time standalone references</h6>
    /// <para>A design-time standalone reference is configured in the same way as a regular one, but adding <c>Standalone="true"</c> attribute to the declaration:</para>
    /// <example>
    ///     <code>
    ///         <calipso:JavaScriptManager ID="jsManager1" CombinedFileName="script1.js" runat="server">
    ///             <References>
    ///                 <calipso:ResourceReference RefId="SomePrefix//MyFile" Standalone="true" /> //<=== Here is the change!
    ///             </References>
    ///         </calipso:JavaScriptManager>
    ///     </code>
    /// </example>
    ///
    /// <h6>Run-time standalone references</h6>
    /// <para>A run-time standalone reference is configured in the same way as a regular one, but giving an additional argument to <see cref="RegisterReference" /> and its optional boolean argument <strong>standalone</strong>.</para>
    /// 
    /// <a name="javascriptmanagermodes" />
    /// <h5>2.1.2 JavaScriptManager debug and release mode: combination and minification</h5>
    /// <para>JavaScriptManager behaves differently depending on ASP.NET execution mode: debug or release.</para>
    /// <list type="table">
    /// <listheader>
    /// <term>Mode</term><description>Description</description>
    /// </listheader>
    /// <item>
    /// <term>Debug</term>
    /// <description>
    /// This mode is activated when regular Web.config has compilation in debug mode (i.e. <c><system.web><compilation debug="true"/></system.web></c>).
    /// Debug mode means that JavaScriptManager will not combine and minify JavaScript dependencies. <strong>IMPORTANT NOTE: Do not use this mode in production environments, since JavaScript file cache is regenerated in every HTTP request.</strong>
    /// </description>
    /// </item>
    /// <item>
    /// <term>Release</term>
    /// <description>
    /// This mode is activated when regular Web.config has compilation in release mode (i.e. <c><system.web><compilation debug="false"/></system.web></c>). 
    /// Debug mode means that JavaScriptManager will combine and minify JavaScript dependencies. <strong>Use this mode either in development or production, but note that once JavaScript files are cached, these are not re-cached anymore but if cached files are
    /// physically removed from the cache directory (i.e. ASP.NET Calipso configuration, "cachePath").</strong>
    /// </description>
    /// </item>
    /// </list>
    /// 
    /// <a name="referenceincludeorder" />
    /// <h5>2.1.3 Order of script inclusion in rendered pages</h5>
    /// <para>JavaScriptManager renders <c><script></script></c> tags for each reference added to itself, either when added from a script control or in design-mode, and <strong>it fully-guarantees that it will render the whole <c><script></script></c>
    /// tag for each reference in addition order.</strong> <c><script></script></c> tags are added in order for each JavaScriptManager references.</para>
    /// 
    /// <a name="dependencyreferences" />
    /// <h5>2.1.4   Dependency references</h5>
    /// <para>Some of includes and references handled by JavaScriptManager may be critical dependencies. That is, <c>SCRIPT</c> tags must be rendered before any other, because the rest need a concrete JavaScript source in order to work properly.</para>
    /// <para>ASP.NET Calipso implements a convention on include identifiers, and it is based on preffixing include identifiers with <strong>"Dependency//"</strong>. Those preffixed with this convention, will end in rendering <c>SCRIPT</c> tags for them before any other.</para>
    /// <para>For example</para>
    /// <code>
    /// <add id="Dependency//Some" source="/Scripts/jquery.js" kind="File" />
    /// </code>
    /// 
    /// <a name="builtindependencies" />
    /// <h4>2.2 Built-in JavaScript dependencies</h4>
    /// <para>JavaScriptManager requires a set of default JavaScript libraries in order to correctly work. Some of them are embedded into <strong>Comkarl.Web.dll</strong> assembly, and others are referenced and must be included in each project, because
    /// ASP.NET Calipso should avoid those as embedded scripts in order to let projects decide when to upgrade them.
    /// </para>
    /// <para>Such libraries that are not embedded into <strong>Comkarl.Web.dll</strong> must be added as includes, either from a script control or by editing <strong>Calipso.config</strong> configuration file and adding a static include. For
    /// example, as <strong>jQuery is a non-built-in dependency but ASP.NET Calipso infrastructure references it</strong>, it must be added as an include:</para>
    /// <code>
    /// <add id="Dependency//jquery" source="/Scripts/jquery.js" kind="File" />
    /// </code>
    /// <br />
    /// <list type="table">
    ///     <listheader><term>Dependency name   </term><description>Include identifier + built-in (yes/no)</description></listheader>
    ///     <item>
    ///         <term>jQuery</term>
    ///         <description>Dependency//jquery (<strong>Built-in: NO</strong>)</description>
    ///     </item>
    ///     <item>
    ///         <term>jOOPL</term>
    ///         <description>Dependency//joopl (<strong>Built-in: YES</strong>)</description>
    ///     </item>
    ///     <item>
    ///         <term>JSON.js</term>
    ///         <description>Dependency//json (<strong>Built-in: YES</strong>)</description>
    ///     </item>
    ///     <item>
    ///         <term>DOM Session Storage helper</term>
    ///         <description>Dependency//session-storage (<strong>Built-in: YES</strong>)</description>
    ///     </item>
    ///     <item>
    ///         <term>Base script control class</term>
    ///         <description>Dependency//client-script-control (<strong>Built-in: YES</strong>)</description>
    ///     </item>
    ///     <item>
    ///         <term>PageContext class</term>
    ///         <description>Dependency//page-context (<strong>Built-in: YES</strong>)</description>
    ///     </item>
    ///     <item>
    ///         <term>PageManager class</term>
    ///         <description>Dependency//page-manager (<strong>Built-in: YES</strong>)</description>
    ///     </item>
    ///     <item>
    ///         <term>sprintf function</term>
    ///         <description>Dependency//sprintf (<strong>Built-in: YES</strong>)</description>
    ///     </item>
    /// </list>
    /// 
    /// <a name="overridingincludes" />
    /// <h4>2.3     Overridding includes</h4>
    /// <para>In some situations, a very concrete requirements makes that, for example, the embedded JavaScript that comes with a script control library should be replaced by a custom one (note: this talks about an embedded one as a sample, it can be
    /// of any kind). In JavaScriptManager, who register first an include wins. Later tries to register the same include with the same identifier will not take any effect.</para>
    /// <para>Above explained behavior (first include to be registered, wins) can be used to override includes. <strong>The best place to do such override is Calipso.config and static includes</strong>, because these are which are registered first before
    /// any script control or any other tries to register some include.</para>
    /// <para>
    /// Overridding an include is as easy as add other include in <strong>Calipso.config</strong> JavaSriptManager's static includes <strong>with the exact identifier of the include to be overridden.</strong>
    /// </para>
    /// <para>For example, maybe a script control called "HelloWorld" registers an include and reference during run-time and its identifier is "Controls//HelloWorld". Editing <strong>Calipso.config</strong> and adding this include will be enough to override the original one:</para>
    /// <code>
    /// <add id="Controls//HelloWorld" source="/Scripts/MyCustomizedHelloWorldControl.js" kind="File" />
    /// </code>
    /// </remarks>
    [ParseChildren(true)]
    public class JavaScriptManager : Comkarl.Web.UI.Controls.WebControl
    {
        /// <summary>
        /// Default constructor.
        /// </summary>
        public JavaScriptManager()
        {
            _references = new List<ResourceReference>();
            _controlRegisterScripts = new List<string>();
        }

        static JavaScriptManager()
        {
            _dynamicIncludes = new List<IncludeElement>();
        }

        private static volatile ICollection<IncludeElement> _dynamicIncludes;
        private readonly List<ResourceReference> _references;
        private readonly List<string> _controlRegisterScripts;

        #region Private properties
        /// <summary>
        /// Gets static JavaScript includes. These are ones acquired from ASP.NET Calipso configuration file (usually "Calipso.config").
        /// </summary>
        private static ReadOnlyCollection<IncludeElement> StaticIncludes
        {
            get
            {
                return new ReadOnlyCollection<IncludeElement>(((CalipsoSection)WebConfigurationManager.GetSection("calipso")).JavaScriptManager.Includes.Cast<IncludeElement>().ToList());
            }
        }

        /// <summary>
        /// Gets dynamic JavaScript includes. These are ones acquired during run-time. That is, ones that are added using
        /// <see cref="RegisterInclude"/> method.
        /// </summary>
        private ICollection<IncludeElement> DynamicIncludes
        {
            get
            {
                return _dynamicIncludes;
            }
        }

        private ReadOnlyCollection<IncludeElement> AllIncludes
        {
            get
            {
                List<IncludeElement> includes = new List<IncludeElement>();
                includes.AddRange(StaticIncludes);
                includes.AddRange(DynamicIncludes);

                return new ReadOnlyCollection<IncludeElement>(includes);
            }
        }

        /// <summary>
        /// Gets a list of already registered script references.
        /// </summary>
        private static List<string> AlreadyRegisteredReferences
        {
            get
            {
                if (HttpContext.Current.Items["JavaScriptManager.AlreadyRegisteredReferences"] == null)
                {
                    HttpContext.Current.Items.Add("JavaScriptManager.AlreadyRegisteredReferences", new List<string>());
                }

                return (List<string>)HttpContext.Current.Items["JavaScriptManager.AlreadyRegisteredReferences"];
            }
        }

        /// <summary>
        /// Gets a dictionary of scripts to include "as is" in the page where current manager is located at.
        /// </summary>
        private Dictionary<string, Tuple<bool, string>> Scripts
        {
            get
            {
                string managerId = "JavaScriptManager." + ID + ".Scripts";

                if (HttpContext.Current.Items[managerId] == null)
                {
                    HttpContext.Current.Items.Add(managerId, new Dictionary<string, Tuple<bool, string>>());
                }

                return (Dictionary<string, Tuple<bool, string>>)HttpContext.Current.Items[managerId];
            }
        }

        private static string jQueryDependencyId
        {
            get { return CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "jquery"; }
        }

        private static string jOOPLDependencyId
        {
            get { return CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "joopl"; }
        }

        #endregion Private properties

        #region Internal properties
        internal List<string> ControlRegisterScripts
        {
            get 
            {
                return _controlRegisterScripts; 
            }
        }
        #endregion

        #region Public properties
        /// <summary>
        /// Gets or sets combined file name. Combination and minification occurs when
        /// Web.config has <code><compilation debug="false" /></code>
        /// </summary>
        public string CombinedFileName
        {
            get;
            set;
        }

        /// <summary>
        /// Gets all JavaScript file references. Accumulates references specified by design-time added references (i.e. in the
        /// server markup - pages and master pages - and ones added by calling <see cref="RegisterReference"/> method).
        /// </summary>
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public List<ResourceReference> References
        {
            get
            {
                return _references;
            }
        }
        #endregion

        #region Protected methods
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            RegisterClientScripts();
        }

        protected override void RenderControl(XmlWriter writer)
        {
            if (ControlRegisterScripts.Count > 0)
            {
                RegisterAfterLoadScript(string.Format("{0}_controlRegisters", ID), string.Join("\n", ControlRegisterScripts.ToArray()));
            }

            if (Scripts.Count > 0)
            {
                string tempRef = null;
                string afterLoadScriptIdFormat = ((CalipsoSection)WebConfigurationManager.GetSection("calipso")).JavaScriptManager.AfterLoadConventionPrefix + "//{0}";
                string tempFileLocation = null;
                string tempFileName = null;

                foreach (KeyValuePair<string, Tuple<bool, string>> script in Scripts)
                {
                    tempFileName = FileNameUtility.ChangeVersionInName(script.Key + ".js", CalipsoSection.Current.JavaScriptManager.CacheVersion);
                    tempFileLocation = Path.Combine(RuntimeEnvironment.JsCacheAbsolutePath, tempFileName);

                    if (HttpContext.Current.IsDebuggingEnabled || !File.Exists(tempFileLocation))
                    {
                        try
                        {
                            File.WriteAllText
                            (
                                tempFileLocation,
                                script.Value.Item2
                            );
                        }
                        catch (IOException)
                        {
                        }
                    }

                    tempRef = string.Format(afterLoadScriptIdFormat, script.Key);

                    RegisterInclude
                    (
                        tempRef,
                        VirtualPathUtility.Combine(RuntimeEnvironment.JsCacheRelativePath, tempFileName),
                        ResourceKind.File
                    );

                    RegisterReference(tempRef, script.Value.Item1);
                }
            }

            if (HttpContext.Current.IsDebuggingEnabled)
            {
                string includeSource = null;
                IncludeElement include = null;

                bool first = true;

                Func<ResourceReference, bool> IsJQueryOrjOOPL = reference => reference.RefId == jQueryDependencyId || reference.RefId == jOOPLDependencyId;

                List<ResourceReference> references = References.Where(reference => IsJQueryOrjOOPL(reference)).ToList();
                references.AddRange(References.Where(reference => !IsJQueryOrjOOPL(reference)));

                foreach (ResourceReference reference in references)
                {
                    if (!AlreadyRegisteredReferences.Contains(reference.RefId))
                    {
                        include = AllIncludes.Single(some => some.Id == reference.RefId);
                        includeSource = include.Source;

                        if (first)
                        {
                            writer.WriteRaw("\n");
                            first = false;
                        }

                        WriteScriptElement(writer, includeSource);
                        writer.WriteRaw("\n");

                        AlreadyRegisteredReferences.Add(reference.RefId);
                    }
                }
            }
            else
            {
                CombineReferences(writer);
            }

            base.RenderControl(writer);
        }

        #endregion Protected methods

        #region Private methods
        private static void WriteScriptElement(XmlWriter writer, string source)
        {
            writer.WriteStartElement("script");
            writer.WriteAttributeString("type", "text/javascript");
            writer.WriteAttributeString("src", source);
            writer.WriteFullEndElement();
        }

        /// <summary>
        /// Registers built-in, out-of-the-box script libraries.
        /// </summary>
        private void RegisterClientScripts()
        {
            RegisterjQuery();
            RegisterSprintf();
            RegisterjOOPL();
            RegisterClientScriptControl();
            RegisterPageManager();
            RegisterPageContext();
        }

        private void RegisterjQuery()
        {
            bool standalone = false;

            if (AllIncludes.Count(include => include.Kind == ResourceKind.ExternalUrl) > 0)
            {
                standalone = true;
            }

            RegisterReference(jQueryDependencyId, standalone);
        }

        private void RegisterSprintf()
        {
            RegisterInclude
            (
                CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "sprintf",
                "Comkarl.Web.ClientScripts.sprintf.js, Comkarl.Web",
                ResourceKind.Resource
            );

            RegisterReference(CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "sprintf");
        }

        private void RegisterjOOPL()
        {
            RegisterReference(jOOPLDependencyId);
        }

        private void RegisterPageManager()
        {
            RegisterInclude
            (
                CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "page-manager",
                "Comkarl.Web.UI.ClientScripts.PageManager.class.js, Comkarl.Web",
                ResourceKind.Resource
            );

            RegisterReference(CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "page-manager");
        }

        private void RegisterPageContext()
        {
            RegisterInclude
            (
                CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "page-context",
                "Comkarl.Web.UI.ClientScripts.PageContext.class.js, Comkarl.Web",
                ResourceKind.Resource
            );

            RegisterReference(CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "page-context");
        }

        private void RegisterClientScriptControl()
        {
            RegisterInclude
            (
                CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "client-script-control",
                "Comkarl.Web.UI.Controls.ClientScripts.Control.class.js, Comkarl.Web",
                ResourceKind.Resource
            );

            RegisterReference(CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "client-script-control");
        }

        /// <summary>
        /// Combines all script references into one file.
        /// </summary>
        /// <param name="xmlWriter"></param>
        private void CombineReferences(XmlWriter xmlWriter)
        {
            CalipsoSection calipsoSection = (CalipsoSection)WebConfigurationManager.GetSection("calipso");
            string combinedFilePath = Path.Combine(RuntimeEnvironment.JsCacheAbsolutePath, FileNameUtility.ChangeVersionInName(CombinedFileName, CalipsoSection.Current.JavaScriptManager.CacheVersion));

            string resourceConventionPrefix = calipsoSection.JavaScriptManager.ResourceConventionPrefix.ToLower();

            bool jQueryIsStandalone = References.Count(reference => reference.RefId.Contains(CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix + "jquery") && reference.Standalone) > 0;

            List<string> excludedReferences = References.Where(reference => reference.Standalone || reference.RefId.ToLower().StartsWith(resourceConventionPrefix))
                                                            .Select(reference => reference.RefId).ToList();

            IEnumerable<ResourceReference> references = References.Where(reference => !AlreadyRegisteredReferences.Contains(reference.RefId) && !excludedReferences.Contains(reference.RefId));

            if (!File.Exists(combinedFilePath))
            {
                using (FileStream stream = File.Create(combinedFilePath))
                using (StreamWriter writer = new StreamWriter(stream))
                {
                    IncludeElement tempInclude = null;
                    string tempVirtualPath = null;

                    foreach (ResourceReference reference in references)
                    {
                        if (!reference.Standalone)
                        {
                            tempInclude = AllIncludes.SingleOrDefault(include => include.Id == reference.RefId && include.Kind != ResourceKind.ExternalUrl);

                            if (tempInclude != null)
                            {
                                tempVirtualPath = VirtualPathUtility.ToAppRelative(AllIncludes.Single(include => include.Id == reference.RefId).Source);

                                if (tempVirtualPath.First() != '~')
                                {
                                    tempVirtualPath = '~' + tempVirtualPath;
                                }

                                writer.WriteLine
                                (
                                    File.ReadAllText
                                    (
                                        HostingEnvironment.MapPath(tempVirtualPath)
                                    )
                                );

                                writer.WriteLine();
                                writer.WriteLine();
                            }

                            writer.WriteLine();
                        }
                    }
                }

                File.WriteAllText(combinedFilePath, JavaScriptCompressor.Current.Compress(File.ReadAllText(combinedFilePath)));
            }

            AlreadyRegisteredReferences.AddRange(references.Select(reference => reference.RefId));

            if (jQueryIsStandalone)
            {
                IncludeElement jQueryInclude = AllIncludes.Single(include => include.Id == jQueryDependencyId);

                WriteScriptElement(xmlWriter, jQueryInclude.Source);
                xmlWriter.WriteRaw("\n");

                AlreadyRegisteredReferences.Add(jQueryDependencyId);
            }

            WriteScriptElement(xmlWriter, VirtualPathUtility.Combine(RuntimeEnvironment.JsCacheRelativePath, FileNameUtility.ChangeVersionInName(CombinedFileName, CalipsoSection.Current.JavaScriptManager.CacheVersion)));
            xmlWriter.WriteRaw("\n");

            List<IncludeElement> nonCombinableIncludes = References.Where(reference => AllIncludes.Single(some => some.Id == reference.RefId).Kind == ResourceKind.ExternalUrl)
                    .Select(reference => AllIncludes.Single(some => some.Id == reference.RefId)).ToList();

            nonCombinableIncludes.AddRange
            (
                AllIncludes.Where(include => excludedReferences.Contains(include.Id))
            );

            foreach (IncludeElement include in nonCombinableIncludes)
            {
                if (include.Id != jQueryDependencyId || !jQueryIsStandalone)
                {
                    WriteScriptElement(xmlWriter, include.Source);
                    xmlWriter.WriteRaw("\n");
                }
            }
        }

        /// <summary>
        /// Serializes an include which is an embedded resource as a file in the physical script's file path.
        /// </summary>
        /// <param name="include">
        /// The whole JavaScript include to serialize.
        /// </param>
        private void SerializeEmbeddedResourceInclude(IncludeElement include)
        {
            string resourcePath = include.Source.Substring(0, include.Source.IndexOf(','));
            string tempFilePath = Path.Combine(RuntimeEnvironment.JsCacheAbsolutePath, resourcePath);

            if (HttpContext.Current.IsDebuggingEnabled || !File.Exists(tempFilePath))
            {
                string assemblyName = include.Source.Substring(include.Source.IndexOf(',') + 1);
                string jsText = Assembly.Load(assemblyName).GetEmbeddedResourceAsString(resourcePath);

                File.WriteAllText(FileNameUtility.ChangeVersionInName(tempFilePath, CalipsoSection.Current.JavaScriptManager.CacheVersion), jsText);
            }

            include.Source = VirtualPathUtility.Combine(RuntimeEnvironment.JsCacheRelativePath, FileNameUtility.ChangeVersionInName(resourcePath, CalipsoSection.Current.JavaScriptManager.CacheVersion));
        }
        #endregion Private methods

        #region Public methods
        /// <summary>
        /// Registers an include to some JavaScript file, which can be a physical one hosted in ASP.NET application folder or subfolder, 
        /// an embedded file to some assembly or a file in some remote site referenced by an URL.
        /// </summary>
        /// <remarks>
        /// An include is a pointer to a JavaScript file. A file can be of these kinds (kind is specified by giving a value of <see cref="Comkarl.Web.Resources.ResourceKind"/> enumeration to
        /// <paramref name="kind"/> input parameter):
        /// <list>
        /// <item><b><see cref="Comkarl.Web.Resources.ResourceKind"/>.File</b></item>
        /// <item><b><see cref="Comkarl.Web.Resources.ResourceKind"/>.Resource</b></item>
        /// <item><b><see cref="Comkarl.Web.Resources.ResourceKind"/>.ExternalUrl</b></item>
        /// </list>
        /// 
        /// Depending on the kind, <paramref name="source"/> parameter sould be given with different formats:
        /// <list>
        ///     <item><term><b>File</b></term>. In this case, it should be an ASP.NET application relative path to the file. For example: /Scripts/MyFile.js</item>
        ///     <item><term><b>Resource</b></term>. Embedded resources should be specified by providing the full assembly qualified name of the resource. For example: "Path.To.Resource.js, MyAssemblyName"</item>
        ///     <item><term><b>ExternalUrl</b></term>External URLs are just plain URLs to a file in some remote site. For example: "http://domain.com/myfile.js"</item>
        /// </list>
        /// 
        /// Independtly to the file kind, any include must give an unique <paramref name="id"/>.
        /// </remarks>
        /// <param name="id">
        /// An unique identifier for the include (f.e. "MyApp//jQuery" or just "jQuery").
        /// </param>
        /// <param name="source">
        /// The source.
        /// </param>
        /// <param name="kind">
        /// The kind (see <see cref="Comkarl.Web.Resources.ResourceKind"/>).
        /// ></param>
        public void RegisterInclude(string id, string source, ResourceKind kind)
        {
            if (AllIncludes.Count(include => include.Id == id) == 0)
            {
                IncludeElement include = new IncludeElement { Id = id, Source = source, Kind = kind };

                if (include.Kind == ResourceKind.Resource)
                {
                    SerializeEmbeddedResourceInclude(include);
                }
                else if (include.Kind == ResourceKind.File && !include.Source.StartsWith(HostingEnvironment.ApplicationVirtualPath))
                {
                    include.Source = VirtualPathUtility.Combine((HostingEnvironment.ApplicationVirtualPath + "/").Replace("//", "/"), include.Source.TrimStart('/'));
                }

                DynamicIncludes.Add(include);
            }
        }

        /// <summary>
        /// Registers a reference to some include.
        /// </summary>
        /// <exception cref="System.ArgumentOutOfRangeException">
        /// Thrown when <paramref name="id"/> does not correspond to an already registered include with the same identifier.
        /// </exception>
        /// <param name="id">
        /// The whole reference identifier to an already registered include.
        /// </param>
        /// <param name="standalone">
        /// Specifies if reference must be treated as standalone. 
        /// </param>
        public void RegisterReference(string id, bool standalone = false)
        {
            if (AllIncludes.Count(include => include.Id == id) == 1)
            {
                ResourceReference reference = new ResourceReference { RefId = id, Standalone = standalone };

                if (id.ToLower().Contains(CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix.ToLower()))
                {
                    int index = References.LastRefIndex(CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix);

                    if (index < 0)
                    {
                        index = 0;
                    }
                    else if (References[index].RefId.ToLower().Contains(CalipsoSection.Current.JavaScriptManager.DependencyConventionPrefix.ToLower()))
                    {
                        index++;
                    }

                    if (index > References.Count)
                    {
                        References.Add(reference);
                    }
                    else
                    {
                        References.Insert(index, reference);
                    }
                }
                else if (id.ToLower().Contains(CalipsoSection.Current.JavaScriptManager.ResourceConventionPrefix.ToLower()))
                {
                    int index = References.LastIndexOf(reference);

                    if (References.Count > 0 && index >= 0)
                    {
                        References.Insert(++index, reference);
                    }
                    else
                    {
                        References.Add(reference);
                    }
                }
                else
                {
                    References.Add(reference);
                }
            }
            else
            {
                throw new ArgumentOutOfRangeException("id", id, "Cannot register this JavaScript reference: there is no include corresponding to given reference identifier");
            }
        }

        /// <summary>
        /// Registers an arbitrary script that will be executed when client-side JavaScript infrastructure gets loaded.
        /// </summary>
        /// <param name="id">An unique identifier</param>
        /// <param name="codeText">The source code text</param>
        /// <param name="standalone">Specifies if it should be registered as an standalone reference</param>
        public void RegisterAfterLoadScript(string id, string codeText, bool standalone = false)
        {
            Scripts.Add(id, Tuple.Create(standalone, codeText));
        }

        /// <summary>
        /// Registers an client-side, arbitrary JavaScript object from CLR anonymous one.
        /// </summary>
        /// <param name="some">
        /// The whole object to register.
        /// </param>
        /// <param name="clientTypeName">
        /// Its client-side full namespace path and name.
        /// </param>
        public void RegisterObject(object some, string clientTypeName)
        {
            if (some != null)
            {
                string typeFileFormat = FileNameUtility.ChangeVersionInName("{0}.clientObject.js", CalipsoSection.Current.JavaScriptManager.CacheVersion);
                string typeFileName = string.Format(typeFileFormat, clientTypeName);
                string fullFilePath = Path.Combine(RuntimeEnvironment.JsCacheAbsolutePath, typeFileName);

                if (Context.IsDebuggingEnabled || !File.Exists(fullFilePath))
                {
                    try
                    {
                        File.WriteAllText
                        (
                            fullFilePath,
                            string.Format("{1} = \n {0};", JsonSerializer.Current.Serialize(some), clientTypeName)
                        );
                    }
                    catch (IOException)
                    { }
                }

                string refId = string.Format("classes//clientObjects/{0}", some.GetType().FullName);

                RegisterInclude
                (
                    refId,
                    VirtualPathUtility.Combine(RuntimeEnvironment.JsCacheRelativePath, typeFileName),
                    ResourceKind.File
                );

                RegisterReference(refId);
            }
        }

        internal void Start()
        {
            if (!Directory.Exists(RuntimeEnvironment.JsCacheAbsolutePath))
            {
                Directory.CreateDirectory(RuntimeEnvironment.JsCacheAbsolutePath);
            }

            foreach (IncludeElement include in ((CalipsoSection)WebConfigurationManager.GetSection("calipso")).JavaScriptManager.Includes)
            {
                switch (include.Kind)
                {
                    case ResourceKind.Resource:
                        SerializeEmbeddedResourceInclude(include);
                        break;

                    case ResourceKind.File:
                        include.Source = (HostingEnvironment.ApplicationVirtualPath + include.Source).Replace("//", "/");
                        break;
                }
            }
        }

        #endregion Public methods
    }
}